﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Security.Cryptography;
using System.IO;
using System.IO.Compression;

namespace KrileUpdateArchiver
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            using (var fbd = new FolderBrowserDialog())
            {
                if (fbd.ShowDialog() == DialogResult.OK)
                {
                    textBox1.Text = fbd.SelectedPath;
                }
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            using (var sfd = new SaveFileDialog())
            {
                sfd.Filter = "Krile Update Archive|*.kua";
                if (sfd.ShowDialog() == DialogResult.OK)
                {
                    textBox2.Text = sfd.FileName;
                }
            }
        }

        List<FileCandidate> fc = new List<FileCandidate>();
        private void button3_Click(object sender, EventArgs e)
        {
            if (!textBox1.Text.EndsWith("\\"))
                textBox1.AppendText("\\");
            label3.Text = "Collecting file information...";
            label3.Refresh();
            Application.DoEvents();
            fc.Clear();
            var files = Directory.GetFiles(textBox1.Text, "*.*", SearchOption.AllDirectories);
            int ctor = 0;
            foreach (var f in files)
            {
                ctor++;
                fc.Add(new FileCandidate(f));
            }
            label3.Text = "Writing file information...(" + ctor.ToString() + " files)";
            label3.Refresh();
            Application.DoEvents();
            using (var fs = new FileStream(textBox2.Text, FileMode.Create, FileAccess.Write))
            {
                using (var gz = new GZipStream(fs, CompressionMode.Compress))
                {
                    StringBuilder descs = new StringBuilder();
                    foreach (var fcd in fc)
                    {
                        descs.AppendLine(fcd.GetRelativePath(textBox1.Text) + "?" + fcd.GetFileHash() + "?" + fcd.GetFileLength());
                    }
                    var descbytes = Encoding.UTF8.GetBytes(descs.ToString());
                    string magic = "KUA:" + ctor.ToString() + ":" + descbytes.Length.ToString() + ":";
                    var magicbytes = Encoding.UTF8.GetBytes(magic);
                    if (magicbytes.Length > 256)
                        throw new Exception("Archiving error:Too long");
                    gz.Write(magicbytes, 0, magicbytes.Length);
                    byte[] padding = new byte[256 - magicbytes.Length];
                    gz.Write(padding, 0, padding.Length);
                    gz.Write(descbytes, 0, descbytes.Length);
                    int lc = 0;
                    foreach (var fcd in fc)
                    {
                        label3.Text = "Archiving file...(" + lc.ToString() + " / " + ctor.ToString() + " files)";
                        label3.Refresh();
                        Application.DoEvents();
                        lc++;
                        using (var infs = new FileStream(fcd.path, FileMode.Open, FileAccess.Read))
                        {
                            byte[] buffer = new byte[1024];
                            while (true)
                            {
                                //圧縮するファイルからデータを読み込む
                                int readSize = infs.Read(buffer, 0, buffer.Length);
                                //最後まで読み込んだ時は、ループを抜ける
                                if (readSize == 0)
                                    break;
                                //データを圧縮して書き込む
                                gz.Write(buffer, 0, readSize);
                            }
                        }
                    }
                }
            }
            label3.Text = "Generating check sum...";
            label3.Refresh();
            using (var w = new StreamWriter(textBox2.Text + ".md5"))
            {
                w.Write(GetFileHash(textBox2.Text));
            }
            label3.Text = "Archive finished. Unarciving and checking file...";
            label3.Refresh();
            Application.DoEvents();
            using (var fs = new FileStream(textBox2.Text, FileMode.Open,  FileAccess.Read))
            {
                using (var gz = new GZipStream(fs, CompressionMode.Decompress))
                {
                    byte[] buffer = new byte[1024];
                    List<FileData> fd = new List<FileData>();
                    if (gz.Read(buffer, 0, 256) < 256)
                        throw new Exception("Archive is invalid format");
                    var infostr = Encoding.UTF8.GetString(buffer).Split(new[] { ":" }, StringSplitOptions.None);
                    if (infostr.Length < 3)
                        throw new Exception("Archive is corrupted");
                    if (infostr[0] != "KUA")
                        throw new Exception("Archive is not supported");
                    int arcnum = int.Parse(infostr[1]);
                    long desclen = long.Parse(infostr[2]);
                    using (MemoryStream ms = new MemoryStream())
                    {
                        ReadStream(gz, ms, desclen);
                        var fds = Encoding.UTF8.GetString(ms.ToArray()).Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
                        using (var sw = new StreamWriter(textBox2.Text + ".txt"))
                        {
                            sw.WriteLine(arcnum.ToString() + "/" + desclen.ToString());
                            foreach (var f in fds)
                            {
                                var sfd = f.Split(new[] { "?" }, StringSplitOptions.None);
                                if (sfd.Length != 3)
                                    throw new Exception("Invalid format");
                                sw.WriteLine("Name:" + sfd[0] + " Length:" + sfd[2] + " Checksum:" + sfd[1]);
                                fd.Add(new FileData() { name = sfd[0], length = long.Parse(sfd[2]), checksum = sfd[1] });
                            }
                        }
                        var destf = Path.Combine(Application.StartupPath, "upd_temp");
                        Directory.CreateDirectory(destf);

                        foreach (var fdi in fd)
                        {
                            var tgfp = Path.Combine(destf, fdi.name);
                            if (!Directory.Exists(Path.GetDirectoryName(tgfp)))
                            {
                                Directory.CreateDirectory(Path.GetDirectoryName(tgfp));
                            }
                            using (var cfs = new FileStream(tgfp, FileMode.Create, FileAccess.ReadWrite))
                            {
                                ReadStream(gz, cfs, fdi.length);
                            }
                            var ifc = new FileCandidate(tgfp);
                            if (ifc.GetFileHash() != fdi.checksum)
                                throw new Exception("Hash not match!");
                        }
                    }
                }
            }
            label3.Text = "Completed!";
            label3.Refresh();
            Application.DoEvents();
        }

        public void ReadStream(Stream ins, Stream outs, long length)
        {
            byte[] buffer = new byte[1024];
            long readedLen = 0;
            while (true)
            {
                long nextRead = length - readedLen;
                if (nextRead > 1024)
                    nextRead = 1024;
                //書庫から展開されたデータを読み込む
                int readSize = ins.Read(buffer, 0, (int)nextRead);
                readedLen += readSize;
                //最後まで読み込んだ時は、ループを抜ける
                if (readSize == 0)
                    return;
                //出力に書き込む
                outs.Write(buffer, 0, readSize);
                if(readedLen >= length)
                    return;
            }
        }

        public class FileData
        {
            public string name;
            public long length;
            public string checksum;
        }

        private class FileCandidate
        {
            public string path;

            public FileCandidate(string path)
            {
                this.path = path;
            }

            public string GetFileHash()
            {
                using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
                {
                    var md5 = new MD5CryptoServiceProvider();
                    var ret = md5.ComputeHash(fs);
                    md5.Clear();
                    return BitConverter.ToString(ret).ToLower().Replace("-", "");
                }
            }

            public string GetRelativePath(string basedir)
            {
                Uri u1 = new Uri(basedir);
                Uri u2 = new Uri(u1, path);
                string relativePath = u1.MakeRelativeUri(u2).ToString();
                //URLデコードして、'/'を'\'に変更する
                relativePath = System.Web.HttpUtility.UrlDecode(relativePath).Replace('/', '\\');
                return relativePath;
            }

            public long GetFileLength()
            {
                var fi = new FileInfo(path);
                return fi.Length;
            }
        }


        public string GetFileHash(string path)
        {
            using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
            {
                var md5 = new MD5CryptoServiceProvider();
                try
                {
                    var ret = md5.ComputeHash(fs);
                    return BitConverter.ToString(ret).ToLower().Replace("-", "");
                }
                finally
                {
                    md5.Clear();
                }
            }
        }

    }
}
